#!/bin/bash

DEFAULT_KEYSIZE=2048
KEYSIZE=$DEFAULT_KEYSIZE

DEFAULT_GTLSSHDIR="$HOME/.gtlssh"
GTLSSHDIR="$DEFAULT_GTLSSHDIR"

DEFAULT_KEYDIR="$GTLSSHDIR/keycerts"
KEYDIR="$DEFAULT_KEYDIR"
KEYDIR_SET=false

DEFAULT_KEYDAYS=365
KEYDAYS=$DEFAULT_KEYDAYS

CONFDIR="@SYSCONFDIR@/gtlssh"

COMMONNAME=
COMMONNAME_SET=false

FORCE=false

help() {
    echo "Key handling tool for gtlssh.  Format is:"
    echo "$1 [<option> [<option> [...]]] command <command options>"
    echo
    echo "Options are:"
    echo "  --keysize <size> - Create an RSA key with the given number of bits."
    echo "        default is $DEFAULT_KEYSIZE"
    echo "  --keydays <days> - Create a key that expires in the given number"
    echo "        of days.  Default is $DEFAULT_KEYDAYS"
    echo "  --basedir <dir> - Base directory for gtlssh.  Default is"
    echo "        $DEFAULT_GTLSSHDIR. Default keys also go here."
    echo "  --keydir <dir> - Location to put the non-default generated keys."
    echo "        Default is $DEFAULT_KEYDIR for normal certificates and"
    echo "        $CONFDIR for server certificates."
    echo "  --commonname <name> - Set the common name in the certificate."
    echo "        The default is your username for normal certificates and"
    echo "        the fully qualified domain name for server certificates."
    echo "  --force, -f - Don't ask questions, just do the operation.  This"
    echo "        may overwrite data without asking."
    echo ""
    echo "Commands are:"
    echo "  setup"
    echo "    Create the directory structure for gtlssh and create the"
    echo "    default keys."
    echo ""
    echo "  keygen [-p <port>] [<hostname>] [[-p <port>] <hostname> [...]]"
    echo "    Generate a keys for the given hostnames, optionally at the given"
    echo "    port.  If no hostname is given, the default key/cert is generated"
    echo "    in"
    echo "      $GTLSSHDIR/default.key"
    echo "    and"
    echo "      $GTLSSHDIR/default.crt"
    echo "    Otherwise the key is generated in"
    echo "      $KEYDIR/<hostname>[,<port>].key/crt."
    echo "    When gtlssh makes a connection, it will look for the hostname"
    echo "    with the port, then just the hostname, then the default key in"
    echo "    that order for the key to use for the connection."
    echo "    If there are any old keys, they will be renamed with a '.1'"
    echo "    appended to the name."
    echo ""
    echo "  rehash [<dir> [<dir> [...]]]"
    echo "    Redo the hash entries in the given directories.  If you put"
    echo "    certificates into those directories but do not rehash them,"
    echo "    the tools will not be able to find the new certificates."
    echo "    If you don't enter any directories, it will rehash the following:"
    echo "      $GTLSSHDIR/allowed_certs"
    echo "      $GTLSSHDIR/server_certs"
    echo "    Certificates that have expired are automatically removed."
    echo ""
    echo "  addallow [-i] <hostname> <file>"
    echo "    Add the given file as an allowed public certificate for the given"
    echo "    hostname.  It will install this file in the directory:"
    echo "      $GTLSSHDIR/allowed_certs"
    echo "    with the name 'hostname.crt'.  It will also rehash the"
    echo "    directory.  If -i is specified, input comes from stdin and the"
    echo "    file is not required or used.  If the destination file already"
    echo "    exists, it will rename it 'hostname.crt.1.crt'."
    echo ""
    echo "  pushcert [-n <name>] [-p <port>] <hostname> [[-p <port>] <hostname> [...]]"
    echo "    Put the local certificate for the given host onto the remote"
    echo "    host so it can be used for login.  It uses old credentials"
    echo "    (credentials with .1 appended to the name, per keygen) if"
    echo "    they are there.  This is useful if you have upated your"
    echo "    certificate and need to send a new one to some remote hosts."
    echo "    It finds the certificate name as described in the keygen"
    echo "    command.  If old credentials exist, it will use those to"
    echo "    connect with gtlssh and send the certificate.  Otherwise it"
    echo "    will use default credentials and hope for the best, probably"
    echo "    only useful if passwords are accepcted.  This only works"
    echo "    one keygen back, if you have run the keygen command twice"
    echo "    for the host, you will need to transfer the certificate"
    echo "    manually.  By default the credential on the remote host is"
    echo "    named the output of 'hostname -f' on the local machine,"
    echo "    -n overrides this."
    echo ""
    echo "  serverkey [name]"
    echo "    Create keys for the gtlsshd server.  Probably requires root."
    echo "    The name is a prefix for they filenames generated which will"
    echo "    be name.crt and name.key.  The default name is 'default'."
}

promptyn() {
    echo -n "$1 (y/n): "
    while true; do
	rsp=""
	read rsp
	case "$rsp" in
	    y)
		return 0
		;;
	    n)
		return 1
		;;
	    *)
		echo "Unknown response, please enter y or n: "
	esac
    done
}

check_dir() {
    if [ ! -d "$2" ]; then
	if $1; then
	    mkdir -m 700 "$2"
	else
	    if $FORCE || promptyn "$2 does not exist.  Create it?"; then
		rm -f "$2"
		mkdir -m 700 "$2"
	    else
		echo "Not creating $2, giving up"
		exit 1
	    fi
	fi
    fi
}

check_dirstruct() {
    created=false
    if [ ! -d "$GTLSSHDIR" ]; then
	if $FORCE || promptyn "$GTLSSHDIR does not exist or is not a directory.  Do you want to correct that?"; then
	    rm -f "$GTLSSHDIR"
	else
	    echo "Not modifying $GTLSSHDIR, giving up"
	    return 1
	fi
	mkdir -m 700 "$GTLSSHDIR"
	created=true
    fi

    check_dir $created "$GTLSSHDIR/keycerts"
    check_dir $created "$GTLSSHDIR/allowed_certs"
    check_dir $created "$GTLSSHDIR/server_certs"
}

hash_dir() {
    if ! ls "$1"/*.crt >/dev/null 2>&1; then
	echo "No certificates in $1, not rehashing"
	return
    fi

    pushd "$1" >/dev/null
    for i in `ls | grep '[0-9a-f]\{8\}\.r\?[0-9]\+'`; do
	rm "$i"
    done
    declare -A fingerprints
    now=`date +'%s'`
    for i in *.crt; do
	# First make sure the certificate hasn't expired.
	datestr=`openssl x509 -noout -enddate -in "$i" | sed 's/^notAfter=//'`
	expiry=`date -d "$datestr" +'%s'`
	if [ $now -ge $expiry ]; then
	    # It's expired, remove it and go on.
	    rm -f "$i"
	    continue
	fi
	fingerprint=`openssl x509 -fingerprint -noout -in "$i"`
	if [ "x${fingerprints[${fingerprint}]}" == 'x' ]; then
	    fingerprints[${fingerprint}]=1
	    hash=`openssl x509 -subject_hash -noout -in "$i"`
	    j=0
	    while [ -e "$hash.$j" ]; do
		j=`expr $j + 1`
	    done
	    ln -sf "$i" "$hash.$j"
	fi
    done
    popd >/dev/null
}

rehash() {
    if [ $# -eq 0 ]; then
	hash_dir "$GTLSSHDIR/allowed_certs"
	hash_dir "$GTLSSHDIR/server_certs"
    else
	while [ $# -gt 0 ]; do
	    if [ ! -d "$1" ]; then
		echo "$1 is not a directory" 1>&2
	    else
		hash_dir "$1"
	    fi
	    shift
	done
    fi
    return 0
}

addallow() {
    stdin=false
    while [ $# -gt 0 ]; do
	case "$1" in
	    -i)
		shift
		stdin=true
		;;
	    --)
		shift
		break
		;;
	    -*)
		echo "Unknown parameter: $1" 1>&2
		return 1
		;;
	    *)
		break;
		;;
	esac
    done
    if [ $# -lt 1 ]; then
	echo "Missing hostname for addallow, see help" 1>&2
	return 1
    fi
    DEST="$GTLSSHDIR/allowed_certs/$1.crt"
    shift
    if ! $stdin && [ $# -lt 1 ]; then
	echo "Missing certificate file for addallow, see help" 1>&2
	return 1
    fi
    if [ -e "$DEST" ]; then
	if $FORCE || promptyn "File $DEST already exists, do you want to overwrite it?"; then
	    mv -f "$DEST" "${DEST}.1.crt"
	else
	    echo "Not installing certificate at $DEST"
	    return 1
	fi
    fi
    if $stdin; then
	if ! cat >${DEST}; then
	    echo "Unable to copy stdin into ${DEST}" 1>&2
	    return 1
	fi
    else
	if ! cp $1 ${DEST}; then
	    echo "Unable to install $1 into ${DEST}" 1>&2
	    return 1
	fi
    fi
    hash_dir "$GTLSSHDIR/allowed_certs"
    return 0
}

keygen_one() {
    if [ -e "$2" -o -e "$3" ]; then
	if $FORCE || promptyn "Files $2 or $3 already exist, do you want to overwrite them?"; then
	    mv -f $2 $2.1
	    mv -f $3 $3.1
	else
	    echo "Not generating key for $1"
	    return 1
	fi
    fi
    openssl req -newkey rsa:"$KEYSIZE" -nodes -keyout "$2" -x509 \
	    -days "$KEYDAYS" -out "$3" -subj "/CN=$COMMONNAME"
    # Just to be sure.
    chmod 600 "$2"
    echo "Key created.  Put $3 into:"
    echo "  .gtlssh/allowed_certs"
    echo "on remote systems you want to log into without a password."
}

keygen() {
    if [ $# -eq 0 ]; then
	keygen_one default "$GTLSSHDIR/default.key" "$GTLSSHDIR/default.crt"
	return $?
    fi

    PORT=""
    while [ $# -gt 0 ]; do
	case "$1" in
	    -p)
		shift
		if [ $# -eq 0 ]; then
		    echo "No port given with -p" 1>&2
		    return 1
		fi
		if [ "X$1" == "X" ]; then
		    PORT=""
		else
		    PORT=",$1"
		fi
		;;

	    *)
		if ! keygen_one $1 "$KEYDIR/$1$PORT.key" "$KEYDIR/$1$PORT.crt"; then
		    return 1
		fi
	esac
	shift
    done
    return 0
}

pushcert_one() {
    HOST="$1"
    PORT="$2"
    NAME="$3"

    if [ -n "$PORT" ]; then
	PPORT="-p $PORT"
    else
	PPORT=""
    fi

    # Find the certificate we want to send.
    if [ -e "$KEYDIR/${HOST}${PORT}.crt" ]; then
	UPCERT="$KEYDIR/${HOST}${PORT}.crt"
    else
	UPCERT="$GTLSSHDIR/default.crt"
    fi

    # Now find the old credentials
    if [ -e "$KEYDIR/${HOST}${PORT}.crt.1" ]; then
	CERT="--certfile $KEYDIR/${HOST}${PORT}.crt.1"
	KEY="-i $KEYDIR/${HOST}${PORT}.key.1"
    elif [ -e "$GTLSSHDIR/default.crt.1" ]; then
	CERT="--certfile $GTLSSHDIR/default.crt.1"
	KEY="-i $GTLSSHDIR/default.key.1"
    else
	CERT=""
	KEY=""
	echo "Could not find an old certificate for ${HOST}${PORT}."
	echo "Just trying to send it without old credentials."
    fi

    gtlssh $KEY $CERT $PPORT "$HOST" \
	   gtlssh-keygen -f addallow -i $NAME <"$UPCERT"
}

pushcert() {
    if [ $# -eq 0 ]; then
	echo "No remote system given to update" 1>&2
	return 1
    fi

    PORT=""
    NAME=`hostname -f`
    while [ $# -gt 0 ]; do
	case "$1" in
	    -p)
		shift
		if [ $# -eq 0 ]; then
		    echo "No port given with -p" 1>&2
		    return 1
		fi
		if [ "X$1" == "X" ]; then
		    PORT=""
		else
		    PORT=",$1"
		fi
		;;

	    -n)
		shift
		if [ $# -eq 0 ]; then
		    echo "No name given with -n" 1>&2
		    return 1
		fi
		if [ "X$1" == "X" ]; then
		    NAME=`hostname -f`
		else
		    NAME="$1"
		fi
		;;

	    *)
		if ! pushcert_one "$1" "$PORT" "$NAME"; then
		    return 1
		fi
	esac
	shift
    done
    return 0
}

serverkey() {
    keyname="gtlsshd"
    if [ $# -gt 0 ]; then
	keyname="$1"
    fi
    SERVERKEY="$CONFDIR/$keyname.key"
    SERVERCERT="$CONFDIR/$keyname.crt"

    if [ -e "$SERVERKEY" -o -e "$SERVERCERT" ]; then
	if $FORCE || promptyn "Files $SERVERKEY or $SERVERCERT already exist, do you want to overwrite them?"; then
	    mv -f "$SERVERKEY" "$SERVERKEY".1
	    mv -f "$SERVERCERT" "$SERVERCERT".1
	else
	    echo "Not generating server keys"
	    return 1
	fi
    fi
    mkdir -p "$CONFDIR"
    if ! openssl req -newkey rsa:"$KEYSIZE" -nodes -keyout "$SERVERKEY" -x509 \
	 -days "$KEYDAYS" -out "$SERVERCERT" -subj "/CN=$COMMONNAME"; then
	echo "Unable to create server key" 1>&2
	return 1
    fi
    # Just to be sure.
    chmod 600 "$SERVERKEY"
    return 0
}

while [ $# -gt 0 ]; do
    case "$1" in
	--keysize)
	    shift
	    if [ $# -eq 0 ]; then
		echo "No keysize given" 1>&2
		exit 1
	    fi
	    KEYSIZE="$1"
	    ;;

	--keydays)
	    shift
	    if [ $# -eq 0 ]; then
		echo "No days given" 1>&2
		exit 1
	    fi
	    KEYDAYS="$1"
	    ;;

	--keydir)
	    shift
	    if [ $# -eq 0 ]; then
		echo "No directory given" 1>&2
		exit 1
	    fi
	    KEYDIR="$1"
	    KEYDIR_SET=true
	    ;;

	--commonname)
	    shift
	    if [ $# -eq 0 ]; then
		echo "No commonname given" 1>&2
		exit 1
	    fi
	    COMMONNAME="$1"
	    COMMONNAME_SET=true
	    ;;

	--force | -f)
	    FORCE=true
	    ;;

	--help | -h | -\?)
	    help "$0"
	    exit 0
	    ;;

	-*)
	    echo "Unknown option '$1'" 1>&2
	    exit 1
	    ;;

	*)
	    break
	    ;;
    esac
    shift
done

RV=0
case "$1" in
    rehash)
	shift
	rehash $@
	RV=$?
	;;

    addallow)
	shift
	addallow $@
	RV=$?
	;;

    pushcert)
	shift
	pushcert $@
	RV=$?
	;;

    keygen)
	shift
	if ! ${KEYDIR_SET}; then
	    check_dirstruct
	fi
	if ! ${COMMONNAME_SET}; then
	    COMMONNAME=`whoami`
	fi
	keygen $@
	RV=$?
	;;

    setup)
	if ! ${COMMONNAME_SET}; then
	    COMMONNAME=`whoami`
	fi
	check_dirstruct
	keygen
	RV=$?
	;;

    serverkey)
	shift
	if ${KEYDIR_SET}; then
	    CONFDIR="${KEYDIR}"
	fi
	if ! ${COMMONNAME_SET}; then
	    COMMONNAME=`hostname -f`
	fi
	serverkey $@
	RV=$?
	;;

    *)
	echo "Unknown command '$1'" 1>&2
	help
	RV=1
	;;
esac
exit ${RV}
